libobs_wrapper\data\output/
mod.rs

1use std::ffi::CString;
2use std::sync::Arc;
3use std::{ffi::CStr, ptr};
4
5use anyhow::bail;
6use getters0::Getters;
7use libobs::{
8    audio_output, obs_encoder_set_audio, obs_encoder_set_video, obs_output, obs_output_active,
9    obs_output_create, obs_output_get_last_error, obs_output_release, obs_output_set_audio_encoder,
10    obs_output_set_video_encoder, obs_output_start, obs_output_stop, obs_output_update,
11    video_output,
12};
13
14use crate::enums::ObsOutputStopSignal;
15use crate::runtime::ObsRuntime;
16use crate::unsafe_send::Sendable;
17use crate::utils::async_sync::RwLock;
18use crate::utils::{AudioEncoderInfo, OutputInfo, VideoEncoderInfo};
19use crate::{impl_obs_drop, impl_signal_manager, run_with_obs, rx_recv};
20
21use crate::{
22    encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder},
23    utils::{ObsError, ObsString},
24};
25
26use super::ObsData;
27
28mod replay_buffer;
29pub use replay_buffer::*;
30
31#[derive(Debug)]
32struct _ObsDropGuard {
33    output: Sendable<*mut obs_output>,
34    runtime: ObsRuntime,
35}
36
37impl_obs_drop!(_ObsDropGuard, (output), move || unsafe {
38    obs_output_release(output);
39});
40
41#[derive(Debug, Getters, Clone)]
42#[skip_new]
43/// A reference to an OBS output.
44///
45/// This struct represents an output in OBS, which is responsible for
46/// outputting encoded audio and video data to a destination such as:
47/// - A file (recording)
48/// - A streaming service (RTMP, etc.)
49/// - A replay buffer
50///
51/// The output is associated with video and audio encoders that convert
52/// raw media to the required format before sending/storing.
53pub struct ObsOutputRef {
54    /// Settings for the output
55    pub(crate) settings: Arc<RwLock<Option<ObsData>>>,
56
57    /// Hotkey configuration data for the output
58    pub(crate) hotkey_data: Arc<RwLock<Option<ObsData>>>,
59
60    /// Video encoders attached to this output
61    #[get_mut]
62    pub(crate) video_encoders: Arc<RwLock<Vec<Arc<ObsVideoEncoder>>>>,
63
64    /// Audio encoders attached to this output
65    #[get_mut]
66    pub(crate) audio_encoders: Arc<RwLock<Vec<Arc<ObsAudioEncoder>>>>,
67
68    /// Pointer to the underlying OBS output
69    #[skip_getter]
70    pub(crate) output: Sendable<*mut obs_output>,
71
72    /// The type identifier of this output
73    pub(crate) id: ObsString,
74
75    /// The unique name of this output
76    pub(crate) name: ObsString,
77
78    /// RAII guard that ensures proper cleanup when the output is dropped
79    #[skip_getter]
80    _drop_guard: Arc<_ObsDropGuard>,
81
82    #[skip_getter]
83    pub(crate) runtime: ObsRuntime,
84
85    pub(crate) signal_manager: Arc<ObsOutputSignals>,
86}
87
88impl ObsOutputRef {
89    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
90    /// Creates a new output reference from the given output info and runtime.
91    ///
92    /// # Arguments
93    /// * `output` - The output information containing ID, name, and optional settings
94    /// * `runtime` - The OBS runtime instance
95    ///
96    /// # Returns
97    /// A Result containing the new ObsOutputRef or an error
98    pub(crate) async fn new(output: OutputInfo, runtime: ObsRuntime) -> Result<Self, ObsError> {
99        let (output, id, name, settings, hotkey_data) = runtime
100            .run_with_obs_result(|| {
101                let OutputInfo {
102                    id,
103                    name,
104                    settings,
105                    hotkey_data,
106                } = output;
107
108                let settings_ptr = match settings.as_ref() {
109                    Some(x) => x.as_ptr(),
110                    None => Sendable(ptr::null_mut()),
111                };
112
113                let hotkey_data_ptr = match hotkey_data.as_ref() {
114                    Some(x) => x.as_ptr(),
115                    None => Sendable(ptr::null_mut()),
116                };
117
118                let output = unsafe {
119                    obs_output_create(
120                        id.as_ptr().0,
121                        name.as_ptr().0,
122                        settings_ptr.0,
123                        hotkey_data_ptr.0,
124                    )
125                };
126
127                if output == ptr::null_mut() {
128                    bail!("Null pointer returned from obs_output_create");
129                }
130
131                return Ok((Sendable(output), id, name, settings, hotkey_data));
132            })
133            .await
134            .map_err(|e| ObsError::InvocationError(e.to_string()))?
135            .map_err(|_| ObsError::NullPointer)?;
136
137        let signal_manager = ObsOutputSignals::new(&output, runtime.clone()).await?;
138        Ok(Self {
139            settings: Arc::new(RwLock::new(settings)),
140            hotkey_data: Arc::new(RwLock::new(hotkey_data)),
141
142            video_encoders: Arc::new(RwLock::new(vec![])),
143            audio_encoders: Arc::new(RwLock::new(vec![])),
144
145            output: output.clone(),
146            id,
147            name,
148
149            _drop_guard: Arc::new(_ObsDropGuard {
150                output,
151                runtime: runtime.clone(),
152            }),
153
154            runtime,
155            signal_manager: Arc::new(signal_manager),
156        })
157    }
158
159    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
160    /// Returns a list of all video encoders attached to this output.
161    ///
162    /// # Returns
163    /// A vector of Arc-wrapped ObsVideoEncoder instances
164    pub async fn get_video_encoders(&self) -> Vec<Arc<ObsVideoEncoder>> {
165        self.video_encoders.read().await.clone()
166    }
167
168    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
169    /// Creates and attaches a new video encoder to this output.
170    ///
171    /// This method creates a new video encoder using the provided information,
172    /// sets up the video handler, and attaches it to this output.
173    ///
174    /// # Arguments
175    /// * `info` - Information for creating the video encoder
176    /// * `handler` - The video output handler
177    ///
178    /// # Returns
179    /// A Result containing an Arc-wrapped ObsVideoEncoder or an error
180    pub async fn video_encoder(
181        &mut self,
182        info: VideoEncoderInfo,
183        handler: Sendable<*mut video_output>,
184    ) -> Result<Arc<ObsVideoEncoder>, ObsError> {
185        let video_enc = ObsVideoEncoder::new(
186            info.id,
187            info.name,
188            info.settings,
189            info.hotkey_data,
190            self.runtime.clone(),
191        )
192        .await?;
193
194        let encoder_ptr = video_enc.encoder.clone();
195        let output_ptr = self.output.clone();
196        let handler = Sendable(handler);
197
198        run_with_obs!(
199            self.runtime,
200            (encoder_ptr, output_ptr, handler),
201            move || unsafe {
202                obs_encoder_set_video(encoder_ptr, handler.0);
203                obs_output_set_video_encoder(output_ptr, encoder_ptr);
204            }
205        )
206        .await?;
207
208        let tmp = Arc::new(video_enc);
209        self.video_encoders.write().await.push(tmp.clone());
210
211        Ok(tmp)
212    }
213
214    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
215    /// Attaches an existing video encoder to this output.
216    ///
217    /// # Arguments
218    /// * `encoder` - The video encoder to attach
219    ///
220    /// # Returns
221    /// A Result indicating success or an error
222    pub async fn set_video_encoder(&mut self, encoder: ObsVideoEncoder) -> Result<(), ObsError> {
223        if encoder.encoder.0.is_null() {
224            return Err(ObsError::NullPointer);
225        }
226
227        let output = self.output.clone();
228        let encoder_ptr = encoder.as_ptr();
229
230        run_with_obs!(self.runtime, (output, encoder_ptr), move || unsafe {
231            obs_output_set_video_encoder(output, encoder_ptr);
232        })
233        .await?;
234
235        if !self
236            .video_encoders
237            .read()
238            .await
239            .iter()
240            .any(|x| x.encoder.0 == encoder.as_ptr().0)
241        {
242            let tmp = Arc::new(encoder);
243
244            self.video_encoders.write().await.push(tmp.clone());
245        }
246
247        Ok(())
248    }
249
250    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
251    /// Updates the settings of this output.
252    ///
253    /// Note: This can only be done when the output is not active.
254    ///
255    /// # Arguments
256    /// * `settings` - The new settings to apply
257    ///
258    /// # Returns
259    /// A Result indicating success or an error
260    pub async fn update_settings(&mut self, settings: ObsData) -> Result<(), ObsError> {
261        let output = self.output.clone();
262        let output_active = run_with_obs!(self.runtime, (output), move || unsafe {
263            obs_output_active(output)
264        })
265        .await?;
266
267        if !output_active {
268            let settings_ptr = settings.as_ptr();
269
270            run_with_obs!(self.runtime, (output, settings_ptr), move || unsafe {
271                obs_output_update(output, settings_ptr)
272            })
273            .await?;
274
275            self.settings.write().await.replace(settings);
276            Ok(())
277        } else {
278            Err(ObsError::OutputAlreadyActive)
279        }
280    }
281
282    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
283    /// Creates and attaches a new audio encoder to this output.
284    ///
285    /// This method creates a new audio encoder using the provided information,
286    /// sets up the audio handler, and attaches it to this output at the specified mixer index.
287    ///
288    /// # Arguments
289    /// * `info` - Information for creating the audio encoder
290    /// * `mixer_idx` - The mixer index to use (typically 0 for primary audio)
291    /// * `handler` - The audio output handler
292    ///
293    /// # Returns
294    /// A Result containing an Arc-wrapped ObsAudioEncoder or an error
295    pub async fn audio_encoder(
296        &mut self,
297        info: AudioEncoderInfo,
298        mixer_idx: usize,
299        handler: Sendable<*mut audio_output>,
300    ) -> Result<Arc<ObsAudioEncoder>, ObsError> {
301        let audio_enc = ObsAudioEncoder::new(
302            info.id,
303            info.name,
304            info.settings,
305            mixer_idx,
306            info.hotkey_data,
307            self.runtime.clone(),
308        )
309        .await?;
310
311        let encoder_ptr = audio_enc.encoder.clone();
312        let output_ptr = self.output.clone();
313
314        run_with_obs!(
315            self.runtime,
316            (handler, encoder_ptr, output_ptr),
317            move || unsafe {
318                obs_encoder_set_audio(encoder_ptr, handler);
319                obs_output_set_audio_encoder(output_ptr, encoder_ptr, mixer_idx);
320            }
321        )
322        .await?;
323
324        let x = Arc::new(audio_enc);
325        self.audio_encoders.write().await.push(x.clone());
326        Ok(x)
327    }
328
329    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
330    /// Attaches an existing audio encoder to this output at the specified mixer index.
331    ///
332    /// # Arguments
333    /// * `encoder` - The audio encoder to attach
334    /// * `mixer_idx` - The mixer index to use (typically 0 for primary audio)
335    ///
336    /// # Returns
337    /// A Result indicating success or an error
338    pub async fn set_audio_encoder(
339        &mut self,
340        encoder: ObsAudioEncoder,
341        mixer_idx: usize,
342    ) -> Result<(), ObsError> {
343        if encoder.encoder.0.is_null() {
344            return Err(ObsError::NullPointer);
345        }
346
347        let encoder_ptr = encoder.encoder.clone();
348        let output_ptr = self.output.clone();
349        run_with_obs!(self.runtime, (output_ptr, encoder_ptr), move || unsafe {
350            obs_output_set_audio_encoder(output_ptr, encoder_ptr, mixer_idx)
351        })
352        .await?;
353
354        if !self
355            .audio_encoders
356            .read()
357            .await
358            .iter()
359            .any(|x| x.encoder.0 == encoder.encoder.0)
360        {
361            let tmp = Arc::new(encoder);
362            self.audio_encoders.write().await.push(tmp.clone());
363        }
364
365        Ok(())
366    }
367
368    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
369    /// Starts the output.
370    ///
371    /// This begins the encoding and streaming/recording process.
372    ///
373    /// # Returns
374    /// A Result indicating success or an error (e.g., if the output is already active)
375    pub async fn start(&self) -> Result<(), ObsError> {
376        let output_ptr = self.output.clone();
377        let output_active = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
378            obs_output_active(output_ptr)
379        })
380        .await?;
381
382        if !output_active {
383            let res = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
384                obs_output_start(output_ptr)
385            })
386            .await?;
387
388            if res {
389                return Ok(());
390            }
391
392            let err = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
393                Sendable(obs_output_get_last_error(output_ptr))
394            })
395            .await?;
396
397            let c_str = unsafe { CStr::from_ptr(err.0) };
398            let err_str = c_str.to_str().ok().map(|x| x.to_string());
399
400            return Err(ObsError::OutputStartFailure(err_str));
401        }
402
403        Err(ObsError::OutputAlreadyActive)
404    }
405
406    /// Stops the output.
407    ///
408    /// This ends the encoding and streaming/recording process.
409    /// The method waits for a stop signal and returns the result.
410    ///
411    /// # Returns
412    /// A Result indicating success or an error with details about why stopping failed
413    #[cfg_attr(feature = "blocking", remove_async_await::remove_async_await)]
414    pub async fn stop(&mut self) -> Result<(), ObsError> {
415        let output_ptr = self.output.clone();
416        let output_active = run_with_obs!(self.runtime, (output_ptr), move || unsafe {
417            obs_output_active(output_ptr)
418        })
419        .await?;
420
421        if output_active {
422            let mut rx = self.signal_manager.on_stop().await?;
423            run_with_obs!(self.runtime, (output_ptr), move || unsafe {
424                obs_output_stop(output_ptr)
425            })
426            .await?;
427
428            let signal = rx_recv!(rx).map_err(|_| ObsError::NoSenderError)?;
429            log::debug!("Signal: {:?}", signal);
430            if signal == ObsOutputStopSignal::Success {
431                return Ok(());
432            }
433
434            return Err(ObsError::OutputStopFailure(Some(signal.to_string())));
435        }
436
437        return Err(ObsError::OutputStopFailure(Some(
438            "Output is not active.".to_string(),
439        )));
440    }
441
442    pub fn as_ptr(&self) -> Sendable<*mut obs_output> {
443        self.output.clone()
444    }
445}
446
447pub unsafe fn process_stop_signal(
448    cd: *mut libobs::calldata_t,
449) -> anyhow::Result<ObsOutputStopSignal> {
450    let mut code = 0i64;
451    let code_str = CString::new("code").unwrap();
452    let got_code = libobs::calldata_get_data(
453        cd,
454        code_str.as_ptr(),
455        &mut code as *mut _ as *mut std::ffi::c_void,
456        size_of::<i64>(),
457    );
458
459    if !got_code {
460        bail!("Failed to get code from calldata");
461    }
462
463    let signal = ObsOutputStopSignal::try_from(code as i32);
464    if let Err(e) = signal {
465        bail!("Failed to convert code to ObsOutputStopSignal: {}", e);
466    }
467
468    Ok(signal.unwrap())
469}
470
471impl_signal_manager!(|ptr| libobs::obs_output_get_signal_handler(ptr), ObsOutputSignals for ObsOutputRef<*mut libobs::obs_output>, [
472    "start": {},
473    "stop": {code: crate::enums::ObsOutputStopSignal},
474    "pause": {},
475    "unpause": {},
476    "starting": {},
477    "stopping": {},
478    "activate": {},
479    "deactivate": {},
480    "reconnect": {},
481    "reconnect_success": {},
482]);